<!DOCTYPE html>
<html>
<head>
  <meta charset="utf-8">
  
  <title>rpc框架之SPI机制源码分析 | rongshen</title>
  <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">
  
  
  
<link rel="stylesheet" href="/css/style.css">

  
    
<link rel="stylesheet" href="/css/highlight.css">

  
  <meta name="description" content="基于dubbo实现rpc框架，分析SPI机制，并实现一个">
<meta property="og:type" content="article">
<meta property="og:title" content="rpc框架之SPI机制源码分析">
<meta property="og:url" content="http://example.com/2021/05/27/rpc%E6%A1%86%E6%9E%B6%E4%B9%8BSPI%E6%9C%BA%E5%88%B6%E6%BA%90%E7%A0%81%E5%88%86%E6%9E%90/index.html">
<meta property="og:site_name" content="rongshen">
<meta property="og:description" content="基于dubbo实现rpc框架，分析SPI机制，并实现一个">
<meta property="og:locale" content="en_US">
<meta property="og:image" content="https://cdn.rongshen2020.com/20210527a1.png">
<meta property="og:image" content="https://cdn.rongshen2020.com/20210527a2.png">
<meta property="og:image" content="https://cdn.rongshen2020.com/20210527a3.png">
<meta property="og:image" content="https://cdn.rongshen2020.com/20210527a4.png">
<meta property="og:image" content="https://cdn.rongshen2020.com/20210527a5.png">
<meta property="article:published_time" content="2021-05-27T08:40:48.000Z">
<meta property="article:modified_time" content="2021-05-27T08:50:46.259Z">
<meta property="article:author" content="John Doe">
<meta property="article:tag" content="SPI机制">
<meta name="twitter:card" content="summary">
<meta name="twitter:image" content="https://cdn.rongshen2020.com/20210527a1.png"><meta name="generator" content="Hexo 6.1.0"></head>

<body>
  <div id="wrapper">
    <header id="header">
  <h1 id="title">
    <a href="/">rongshen</a>
  </h1>
  <nav>
    
    
      
      <a class="nav-link" href="/">Home</a>
    
      
        <span class="nav-spacer">×</span>
      
      <a class="nav-link" href="/archives">Archives</a>
    
      
        <span class="nav-spacer">×</span>
      
      <a class="nav-link" target="_blank" rel="noopener" href="https://github.com/Aaron-boom">Github</a>
    
      
        <span class="nav-spacer">×</span>
      
      <a class="nav-link" href="/2020/04/13/About-Me/">About</a>
    
    
  </nav>
</header>

    <div id="content">
      <article id="post-rpc框架之SPI机制源码分析" class="article article-type-post" itemprop="blogPost" itemscope>
  <div class="article-inner">
    
      <header class="article-header">
        
  
    <h2 class="article-title" itemprop="headline name">
      rpc框架之SPI机制源码分析
    </h2>
  


        <div class="article-meta">
          <time class="article-date" datetime="2021-05-27T08:40:48.000Z" itemprop="datePublished">2021-05-27</time>

          
        </div>
      </header>
    
    <div class="article-entry" itemprop="articleBody">
      
      
        <p>基于dubbo实现rpc框架，分析SPI机制，并实现一个</p>
<span id="more"></span>


<blockquote>
<p>参考：<a target="_blank" rel="noopener" href="http://tianxiaobo.com/2018/10/01/Dubbo-%E6%BA%90%E7%A0%81%E5%88%86%E6%9E%90-SPI-%E6%9C%BA%E5%88%B6/">http://tianxiaobo.com/2018/10/01/Dubbo-%E6%BA%90%E7%A0%81%E5%88%86%E6%9E%90-SPI-%E6%9C%BA%E5%88%B6/</a></p>
<p>javaguide-rpc</p>
</blockquote>
<h1 id="简介："><a href="#简介：" class="headerlink" title="简介："></a>简介：</h1><p>SPI 全称为 Service Provider Interface，是一种服务发现机制。SPI的本质是将接口实现类的全限定名配置在文件中，并由服务加载器读取配置文件，加载实现类。<strong>这样可以在运行时，动态为接口加载实现类。</strong> 正因此特性，我们可以很容易的通过SPI 机制为我们的程序提供拓展功能。SPI 机制在第三方框架中也有所应用，比如 Dubbo 就是通过 SPI机制加载所有的组件。不过，Dubbo 并未使用 Java 原生的 SPI 机制，而是对其进行了增强，使其能够更好的满足需求。</p>
<h1 id="本框架SPI示例："><a href="#本框架SPI示例：" class="headerlink" title="本框架SPI示例："></a>本框架SPI示例：</h1><p>并未使用 Java SPI，而是重新实现了一套SPI 机制。SPI 的相关逻辑被封装在了<br>ExtensionLoader 类中，通过 ExtensionLoader，我们可以加载指定的实现类。<strong>我们将接口的实现类的全限定名配置在文件中，</strong> 放置在META-INF&#x2F;extensions路径下，下面来看一下配置内容。</p>
<p><img src="https://cdn.rongshen2020.com/20210527a1.png"></p>
<p><img src="https://cdn.rongshen2020.com/20210527a2.png"></p>
<ul>
<li>还需要在接口上使用<code>@SPI</code>注解。</li>
</ul>
<p><img src="https://cdn.rongshen2020.com/20210527a3.png"></p>
<h1 id="简单使用："><a href="#简单使用：" class="headerlink" title="简单使用："></a>简单使用：</h1><p>那么在<code>RpcMessageDecoder.java</code>中就可以直接创建一个<code>Compress</code>类了，使用对象中的方法。<code>compressName=gzip</code></p>
<p><img src="https://cdn.rongshen2020.com/20210527a4.png"></p>
<h1 id="源码分析："><a href="#源码分析：" class="headerlink" title="源码分析："></a>源码分析：</h1><p>我们首先通过 ExtensionLoader 的 getExtensionLoader 方法获取一个 ExtensionLoader<br>实例，然后再通过 ExtensionLoader 的 getExtension 方法获取拓展类对象。这其中，getExtensionLoader用于从缓存中获取与拓展类对应的 ExtensionLoader，若缓存未命中，则创建一个新的实例。</p>
<p>那我们稍微了解下SPI机制，实现下大致功能。</p>
<p><img src="https://cdn.rongshen2020.com/20210527a5.png"></p>
<h2 id="Holder-java"><a href="#Holder-java" class="headerlink" title="Holder.java:"></a><code>Holder.java</code>:</h2><pre><code class="java">package com.aaron.extension;

// Holder仅用于持有对象目标，没有其他逻辑
public class Holder&lt;T&gt; &#123;
    /**
     * volatile修饰一个对象的话其实是保证这个对象的引用的可见性，而不是保证这个对象的内容的可见性。
     */
    private volatile T value;

    public T get() &#123;
        return value;
    &#125;

    public void set(T value) &#123;
        this.value = value;
    &#125;
&#125;
</code></pre>
<h2 id="ExtensionLoader-java："><a href="#ExtensionLoader-java：" class="headerlink" title="ExtensionLoader.java："></a><code>ExtensionLoader.java</code>：</h2><pre><code class="java">package com.aaron.extension;


import lombok.extern.slf4j.Slf4j;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.URL;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

import static java.nio.charset.StandardCharsets.UTF_8;

/**
 * 参考的是dubbo spi机制。
 */
@Slf4j
public final class ExtensionLoader&lt;T&gt; &#123;
    // 配置文件path
    private static final String SERVICE_DIRECTORY = &quot;META-INF/extensions/&quot;;
    // 保存的是ExtensionLoader实例，一个拓展类就有一个ExtensionLoader实例吗？
    private static final Map&lt;Class&lt;?&gt;, ExtensionLoader&lt;?&gt;&gt; EXTENSION_LOADERS = new ConcurrentHashMap&lt;&gt;();
    // Extension对象，就是拓展类，key:接口类型，value:实例化对象
    private static final Map&lt;Class&lt;?&gt;, Object&gt; EXTENSION_INSTANCES = new ConcurrentHashMap&lt;&gt;();
    // 某个接口类
    private final Class&lt;?&gt; type;
    // 实例化的拓展对象，key:String,  value:Holder&lt;Object&gt;对象
    private final Map&lt;String, Holder&lt;Object&gt;&gt; cachedInstances = new ConcurrentHashMap&lt;&gt;();
    // 缓存中保存了实例化的拓展类
    private final Holder&lt;Map&lt;String, Class&lt;?&gt;&gt;&gt; cachedClasses = new Holder&lt;&gt;();

    // 构造方法
    private ExtensionLoader(Class&lt;?&gt; type) &#123;
        this.type = type;
    &#125;

    /**
     * 获取ExtensionLoader实例
     * 首先从缓存中获取与拓展类对应的 ExtensionLoader，若缓存未命中，则创建一个新的实例。
     */
    public static &lt;S&gt; ExtensionLoader&lt;S&gt; getExtensionLoader(Class&lt;S&gt; type) &#123;
        // 查看这个类要获取实例时（也就是拓展类），符不符合规则。
        if (type == null) &#123;
            throw new IllegalArgumentException(&quot;Extension type should not be null.&quot;);
        &#125;
        if (!type.isInterface()) &#123;
            throw new IllegalArgumentException(&quot;Extension type must be an interface.&quot;);
        &#125;
        if (type.getAnnotation(SPI.class) == null) &#123;
            throw new IllegalArgumentException(&quot;Extension type must be annotated by @SPI&quot;);
        &#125;
        // 首先在cache中查找，与之对应的实例化对象
        ExtensionLoader&lt;S&gt; extensionLoader = (ExtensionLoader&lt;S&gt;) EXTENSION_LOADERS.get(type);
        // 如果cache没有命中，那么就创建一个新的实例。创建完成之后，再在EXTENSION_LOADERS中获取。
        // putIfAbsent()方法：如果传入key对应的value已经存在，就返回存在的value，不进行替换。如果不存在，就添加key和value，返回null
        if (extensionLoader == null) &#123;
            EXTENSION_LOADERS.putIfAbsent(type, new ExtensionLoader&lt;S&gt;(type));
            extensionLoader = (ExtensionLoader&lt;S&gt;) EXTENSION_LOADERS.get(type);
        &#125;
        return extensionLoader;
    &#125;

    /**
     * 通过ExtensionLoader实例 的getExtension方法获取拓展类对象.
     * 首先检查缓存，缓存未命中则创建拓展对象。
     */
    public T getExtension(String name) &#123;
        // 传入的参数就是：全限定的接口名
        if (name == null || name.isEmpty()) &#123;
            throw new IllegalArgumentException(&quot;Extension name should not be null or empty.&quot;);
        &#125;
        // Holder仅用于持有目标对象，没其他什么逻辑
        Holder&lt;Object&gt; holder = cachedInstances.get(name);

        if (holder == null) &#123;
            cachedInstances.putIfAbsent(name, new Holder&lt;&gt;());
            holder = cachedInstances.get(name);
        &#125;
        // 没有拓展实例不存在的话，就创建一个实例
        Object instance = holder.get();
        if (instance == null) &#123;
            synchronized (holder) &#123;
                instance = holder.get();
                if (instance == null) &#123;
                    // 创建拓展实例
                    instance = createExtension(name);
                    holder.set(instance);
                &#125;
            &#125;
        &#125;
        return (T) instance;
    &#125;

    // 创建拓展对象实例
    private T createExtension(String name) &#123;
        // 从配置文件中加载所有的拓展类，形成配置项名称到配置类的映射关系。得到的是类型
        Class&lt;?&gt; clazz = getExtensionClasses().get(name);
        if (clazz == null) &#123;
            throw new RuntimeException(&quot;No such extension of name &quot; + name);
        &#125;
        // 在缓存中判断是否有这个拓展类对象实例
        T instance = (T) EXTENSION_INSTANCES.get(clazz);
        if (instance == null) &#123;
            try &#123;
                // 创建实例
                EXTENSION_INSTANCES.putIfAbsent(clazz, clazz.newInstance());
                // 创建完再获取
                instance = (T) EXTENSION_INSTANCES.get(clazz);
            &#125; catch (Exception e) &#123;
                log.error(e.getMessage());
            &#125;
        &#125;
        return instance;
    &#125;

    /**
     * 我们在通过名称获取拓展类之前，
     * 首先需要根据配置文件解析出名称到拓展类的映射，也就是 Map&lt;名称, 拓展类&gt;。
     * 之后再从 Map 中取出相应的拓展类即可
     */
    // 这里也是先检查缓存，若缓存未命中，则通过 synchronized 加锁。
    // 加锁后再次检查缓存，并判空。此时如果 classes 仍为 null，则加载拓展类。以上代码的写法是典型的双重检查锁.
    private Map&lt;String, Class&lt;?&gt;&gt; getExtensionClasses() &#123;
        // 从缓存中获取  拓展类（class）
        Map&lt;String, Class&lt;?&gt;&gt; classes = cachedClasses.get();
        // double check
        if (classes == null) &#123;
            synchronized (cachedClasses) &#123;
                classes = cachedClasses.get();
                if (classes == null) &#123;
                    classes = new HashMap&lt;&gt;();
                    // load all extensions from our extensions directory
                    // 从指定文件夹配置文件中 加载所有的拓展类
                    loadDirectory(classes);
                    cachedClasses.set(classes);
                &#125;
            &#125;
        &#125;
        return classes;
    &#125;

    // 从指定文件夹配置文件中 加载所有的拓展类
    private void loadDirectory(Map&lt;String, Class&lt;?&gt;&gt; extensionClasses) &#123;
        // fileName = 文件夹路径 + type 全限定名。所以文件名必须以type的全限定名命名
        String fileName = ExtensionLoader.SERVICE_DIRECTORY + type.getName();
        try &#123;
            Enumeration&lt;URL&gt; urls;
            ClassLoader classLoader = ExtensionLoader.class.getClassLoader();
            // 根据文件名加载所有的同名文件
            urls = classLoader.getResources(fileName);
            //
            if (urls != null) &#123;
                while (urls.hasMoreElements()) &#123;
                    URL resourceUrl = urls.nextElement();
                    // 加载资源
                    loadResource(extensionClasses, classLoader, resourceUrl);
                &#125;
            &#125;
        &#125; catch (IOException e) &#123;
            log.error(e.getMessage());
        &#125;
    &#125;

    // 加载资源，读取所有的配置文件信息，并全部加载到extensionClasses中
    private void loadResource(Map&lt;String, Class&lt;?&gt;&gt; extensionClasses, ClassLoader classLoader, URL resourceUrl) &#123;
        try (BufferedReader reader = new BufferedReader(new InputStreamReader(resourceUrl.openStream(), UTF_8))) &#123;

            String line;
            // read every line，按行读取配置内容
            while ((line = reader.readLine()) != null) &#123;
                // get index of comment
                final int ci = line.indexOf(&#39;#&#39;);
                if (ci &gt;= 0) &#123;
                    // 截取#之前的字符串，#之后的内容为注释,忽略
                    line = line.substring(0, ci);
                &#125;
                // 去除首位空格
                line = line.trim();
                if (line.length() &gt; 0) &#123;
                    try &#123;
                        final int ei = line.indexOf(&#39;=&#39;);
                        // 以 = 为界，截取键与值。比如 dubbo=com.alibaba.compress.DubboProtocol
                        String name = line.substring(0, ei).trim();
                        String clazzName = line.substring(ei + 1).trim();
                        // 我们的SPI使用键值对，因此它们两个都不能是空的
                        if (name.length() &gt; 0 &amp;&amp; clazzName.length() &gt; 0) &#123;
                            Class&lt;?&gt; clazz = classLoader.loadClass(clazzName);
                            extensionClasses.put(name, clazz);
                        &#125;
                    &#125; catch (ClassNotFoundException e) &#123;
                        log.error(e.getMessage());
                    &#125;
                &#125;
            &#125;

        &#125; catch (IOException e) &#123;
            log.error(e.getMessage());
        &#125;
    &#125;
&#125;
</code></pre>

      
    </div>
    
    
    <div class="article-category">
      
        <b>Categories:</b>
        <a class="article-category-link" href="/categories/RPC%E6%A1%86%E6%9E%B6/">RPC框架</a>
      
      
        <br/>
      
      
        <b>Tags:</b>
        <a class="article-tag-none-link" href="/tags/SPI%E6%9C%BA%E5%88%B6/" rel="tag">SPI机制</a>
      
    </div>
    
    
  </div>
</article>

  
<nav id="article-nav" class="article-nav">
  
    <a href="/2021/06/21/rpc%E6%A1%86%E6%9E%B6-%E8%B4%9F%E8%BD%BD%E5%9D%87%E8%A1%A1%E4%B9%8B%E4%B8%80%E8%87%B4%E6%80%A7hash%E7%AE%97%E6%B3%95-%E4%BB%BFDubbo/" id="article-nav-newer" class="article-nav-link-wrap newer">
      <strong class="article-nav-caption">Newer</strong>
      <div class="article-nav-title">
        
          rpc框架-负载均衡之一致性hash算法(仿Dubbo)
        
      </div>
    </a>
  
  
    <a href="/2021/05/05/%E3%80%8A%E4%B8%80%E4%B8%80%E3%80%8B/" id="article-nav-older" class="article-nav-link-wrap older">
      <strong class="article-nav-caption">Older</strong>
      <div class="article-nav-title">
        
          《一一》
        
      </div>
    </a>
  
</nav>






    </div>
  </div>
  




<div id="settings-container">
  <div id="dark-mode">dark</div>
  <div id="sans-font">sans</div>
</div>
<script type="text/javascript">
let d=document,r=d.documentElement.style,f=r.setProperty.bind(r),l=localStorage,s=l.getItem('s')||(window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches),n=l.getItem('n'),m=d.getElementById("dark-mode"),b=()=>{f('--bg-color','#fafafa');f('--code-bg-color','#f4f4f4');f('--text-color','#212121');f('--secondary-color','#808080');f('--tertiary-color','#b0b0b0');f('--link-color','#b5c8cf');f('--link-hover-color','#618794');f('--link-bg-color','#dae4e7');f('--selection-color','#dae4e7');m.innerHTML="dark"},c=()=>{f('--bg-color','#212121');f('--code-bg-color','#292929');f('--text-color','#fff');f('--secondary-color','#c0c0c0');f('--tertiary-color','#6e6e6e');f('--link-color','#4d6b75');f('--link-hover-color','#96b1bb');f('--link-bg-color','#5d828e');f('--selection-color','#acc1c9');m.innerHTML="light"},o=d.getElementById("sans-font"),e=()=>{f('--body-stack','"Lora", "Georgia", "Times New Roman", serif');o.innerHTML="sans"},g=()=>{f('--body-stack','"Lato", "Lucida Grande", "Lucida Sans Unicode", "Lucida Sans", "Verdana", sans-serif');o.innerHTML="serif"};m.onclick=()=>{if(s==2){s=1;l.setItem('s',s);c()}else{s=2;l.setItem('s',s);b()}};o.onclick=()=>{if(n==2){n=1;l.setItem('n',n);g()}else{n=2;l.setItem('n',n);e()}};if(!s){s=2;l.setItem('s',2)};if(s==1){c()};if(!n){n=2;l.setItem('n',2)};if(n==1){g()};
</script>




</body>
</html>
